package cn.marknum.gateway.filter;

import cn.hutool.core.util.StrUtil;
import cn.marknum.cas.client.authentication.AttributePrincipalImpl;
import cn.marknum.cas.client.session.AssertionStorage;
import cn.marknum.cas.client.validation.Assertion;
import cn.marknum.cas.client.validation.AssertionImpl;
import cn.marknum.gateway.config.CasProperties;
import cn.marknum.web.response.Response;
import cn.marknum.web.response.ResponseDSL;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 用来查询cas的开启状态
 *
 * @author zhangzhao    2021-08-30 16:37
 */
public class CasOpenStateFilter implements WebFilter {

    private static Logger logger = LoggerFactory.getLogger(CasOpenStateFilter.class);
    private CasProperties casProperties;
    private String[] toErrorHtmlUrls;
    private String logoutUrl;

    private AssertionStorage assertionStorage;

    public CasOpenStateFilter(String toErrorHtmlUrls, String logoutUrl, AssertionStorage assertionStorage) {
        this.logoutUrl = logoutUrl;
        this.assertionStorage = assertionStorage;
        if (StrUtil.isNotBlank(toErrorHtmlUrls))
            this.toErrorHtmlUrls = toErrorHtmlUrls.split(",");
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String path = exchange.getRequest().getPath().value();
        if (path.endsWith("/cas/state")) {
            exchange.getResponse().getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            Map<String, Object> result = new HashMap<>();
            result.put("isOpened", casProperties.open);
            for (Map.Entry<String, String> entry : casProperties.client.urls.entrySet()) {
                result.put(entry.getKey(), entry.getValue());
            }
            Response responseResult = ResponseDSL.singleSuccess(result);
            DataBuffer dataBuffer =
                    exchange.getResponse().bufferFactory().wrap(JSONObject.toJSONString(responseResult).getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Flux.just(dataBuffer));
        } else if (path.endsWith("/loginForAppMgr")) {
            exchange = exchange.mutate().response(new HandlerServerHttpResponse(exchange.getResponse())).build();
        }
        for (String url : toErrorHtmlUrls) {
            if (path.contains(url)) {
                return chain.filter(exchange.mutate().response(new ModifiedServerHttpResponse(exchange, logoutUrl)).build());
            }
        }
        Mono<Void> mono = chain.filter(exchange);
        return mono;
    }

    class HandlerServerHttpResponse extends ServerHttpResponseDecorator {

        public HandlerServerHttpResponse(ServerHttpResponse delegate) {
            super(delegate);
        }

        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            return super.writeWith(Flux.from(body)
                    .map(dataBuffer -> {
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        DataBufferUtils.release(dataBuffer);
                        String result = new String(content, StandardCharsets.UTF_8);
                        JSONObject jo = JSON.parseObject(result);
                        if ("0".equals(jo.getString("xym"))) {
                            JSONObject sjXx = jo.getJSONObject("sjXx");
                            Assertion assertion = new AssertionImpl(new AttributePrincipalImpl(sjXx.getString("zh")), new Date(), null, new Date(), new HashMap<>());
                            assertionStorage.put(sjXx.getString("token"), assertion);
                        }
                        return dataBuffer.factory().wrap(content);
                    }));
        }

        @Override
        public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
            return this.writeWith(Flux.from(body).flatMapSequential((p) -> p));
        }
    }

    class ModifiedServerHttpResponse extends ServerHttpResponseDecorator {

        Logger logger = LoggerFactory.getLogger(ModifiedServerHttpResponse.class);
        ServerWebExchange exchange;
        Expression expression;
        String logoutUrl;

        public ModifiedServerHttpResponse(ServerWebExchange exchange, String logoutUrl) {
            super(exchange.getResponse());
            this.exchange = exchange;
            this.logoutUrl = logoutUrl;
            initErrorHtmlTemplate();
        }

        private void initErrorHtmlTemplate() {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream("error.html"), StandardCharsets.UTF_8))) {
                StringBuilder builder = new StringBuilder();
                while (br.ready()) {
                    builder.append(br.readLine().trim());
                }
                SpelExpressionParser sep = new SpelExpressionParser();
                TemplateParserContext context = new TemplateParserContext();
                expression = sep.parseExpression(builder.toString(), context);
            } catch (Exception e) {
                logger.warn("读取错误页面模板失败", e);
            }
        }

        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            String contentType = this.exchange.getResponse().getHeaders().getFirst("Content-Type");
            //返回内容如果本身是html内容，则不修改内容
            if (contentType.contains(MediaType.TEXT_HTML_VALUE)) {
                return this.getDelegate().writeWith(body);
            }
            //如果不是html内容，则按照发送错误处理，返回error.html
            String originalResponseContentType = this.exchange.getAttribute("original_response_content_type");
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.add("Content-Type", originalResponseContentType);
            ClientResponse response = prepareClientResponse(body, httpHeaders);
            Mono modifiedBody = response.bodyToMono(byte[].class)
                    .flatMap(originalBody -> {
                        String originalStr = new String(originalBody, StandardCharsets.UTF_8);
                        if (originalStr.startsWith("{") && originalStr.endsWith("}")) {
                            Response resp = JSON.parseObject(originalStr, Response.class);
                            originalStr = resp.getXyXx() + "[" + resp.getXym() + "]";
                        }
                        String result = expression.getValue(new HtmlContent(originalStr, logoutUrl), String.class);
                        return Mono.just(result.getBytes(StandardCharsets.UTF_8));
                    });
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, byte[].class);
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(this.exchange, this.exchange.getResponse().getHeaders());
            return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                Mono<DataBuffer> messageBody = DataBufferUtils.join(outputMessage.getBody());
                HttpHeaders headers = this.getDelegate().getHeaders();
                headers.setContentType(MediaType.TEXT_HTML);
                if (!headers.containsKey("Transfer-Encoding") || headers.containsKey("Content-Length")) {
                    messageBody = messageBody.doOnNext((data) -> {
                        headers.setContentLength(data.readableByteCount());
                    });
                }
                return this.getDelegate().writeWith(messageBody);
            }));
        }

        @Override
        public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
            return this.writeWith(Flux.from(body).flatMapSequential((p) -> {
                return p;
            }));
        }

        private ClientResponse prepareClientResponse(Publisher<? extends DataBuffer> body, HttpHeaders httpHeaders) {
            ClientResponse.Builder builder = ClientResponse.create(this.exchange.getResponse().getStatusCode());
            return builder.headers((headers) -> headers.putAll(httpHeaders))
                    .body(Flux.from(body))
                    .build();
        }
    }

    class HtmlContent {
        String message;
        String logoutUrl;

        public HtmlContent() {
        }

        public HtmlContent(String message, String logoutUrl) {
            this.message = message;
            this.logoutUrl = logoutUrl;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        public String getLogoutUrl() {
            return logoutUrl;
        }

        public void setLogoutUrl(String logoutUrl) {
            this.logoutUrl = logoutUrl;
        }
    }
}
